import java.io.BufferedWriter;
import java.io.FileWriter;
import java.io.IOException;
import java.util.Random;

public class SuperLearn {

	static Random rnd = new Random(1);

	static double[] weights = new double[ Tilecoder.numFeatures ];
	static double alpha = 0.1 / Tilecoder.numOfTilings;

	/**
     * Given the current tilecoding and weight vector produced
     * through training on a specific function f, returns an estimate
     * of f(x1,x2).
	 */
	public static double f(double x1, double x2) {
		
		double sum = 0.0;
		
		int[] tilecodeIndices = new int[ Tilecoder.numOfTilings ];
		
		// get tilecode indices for x1, x2 (where phi(i) = 1)
		Tilecoder.tilecode(x1, x2, tilecodeIndices);
		
        // for each tile that x1,x2 falls within...
		for ( int index : tilecodeIndices ) {
			sum += weights[index];
		}
		
		return sum;
	}

	/**
     * Given inputs x1, x2 and observed output y, improve feature
     * weights given the error between the estimated and observed
     * outputs.
	 */
	public static void learn(double x1, double x2, double y) {
		
		int[] tilecodeIndices = new int[ Tilecoder.numOfTilings ];

		// get tilecode indices for x1, x2 (where phi(i) = 1)
		Tilecoder.tilecode(x1, x2, tilecodeIndices);

        // find error between expected and observed mutiliplied by alpha
        double alphaTimesError = alpha * (y - f(x1,x2));
		
        // for each tile that x1,x2 falls within...
		for ( int index : tilecodeIndices ) {
			
			// phi[index] = 1 only for indices in tilecodeIndices
			weights[index] = weights[index] + alphaTimesError;
			
		}
		
	}

	private static void printTest1(double x1, double x2, double y) {
		double fxbeforey = f(x1, x2);
		learn(x1, x2, y);
		double fxaftery = f(x1, x2);
		System.out.println("Example (" + x1 + ", " + x2 + ", " + y + "). f(x) before learning: " + fxbeforey
				+ " and after learning: " + fxaftery);
	}

	public static double targetFunction(double x1, double x2) {
		return Math.sin(x1 - 3) * Math.cos(x2) + rnd.nextGaussian() * 0.1;
	}

	public static void train(int numOfSteps) {
		for (int i = 0; i < numOfSteps; i++) {
			double x1 = rnd.nextDouble() * 6;
			double x2 = rnd.nextDouble() * 6;
			double y = targetFunction(x1, x2);
			learn(x1, x2, y);
		}

	}

	public static void writeF(String filename) {
		FileWriter fstreamout;
		try {
			fstreamout = new FileWriter(filename);
			BufferedWriter out = new BufferedWriter(fstreamout);
			int x1steps = 50, x2steps = 50;
			for (int i = 0; i < x1steps; i++) {
				for (int j = 0; j < x2steps; j++) {
					double y = f(i * 6.0 / x1steps, j * 6.0 / x2steps);
					out.write(y + ",");
				}
				out.write("\n");
			}
			out.close();
		} catch (IOException e) {
			e.printStackTrace();
		}
	}

	public static void printMSE() {
		int N = 10000;
		double totalSE = 0.0;
		for (int i = 0; i < N; i++) {
			double x1 = rnd.nextDouble() * 6.0;
			double x2 = rnd.nextDouble() * 6.0;
			double error = targetFunction(x1, x2) - f(x1, x2);
			totalSE += error * error;
		}
		System.out.println("The estimated MSE: " + totalSE / N);
	}

	public static void test1(String[] args) {
		printTest1(0.1, 0.1, 2.0);
		printTest1(4.0, 2.0, -1.0);
		printTest1(5.99, 5.99, 3.0);
		printTest1(4.0, 2.1, -1.0);
	}

	public static void main(String[] args) {
		train(20);
		writeF("f20.csv");
		printMSE();

		for (int i = 0; i < 10; i++) {
			train(1000);
			printMSE();
		}
		writeF("f10000.csv");
	}
}
